Copyright (C) 2011 Paul Brook, paul@nowt.org
Copyright (C) 2003-2011 Robert Lipe, robertlipe+source@gpsbabel.org
+ Copyright (C) 2019 Martin Buck, mb-tmp-tvguho.pbz@gromit.dyndns.org
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
#include <cstdint>
#include <cstdio> // for EOF, snprintf
+#include <vector>
+#include <deque>
+#include <utility>
#include <QtCore/QDateTime> // for QDateTime
#include <QtCore/QString> // for QString
#define MYNAME "fit"
// constants for global IDs
+const int kIdFileId = 0;
const int kIdDeviceSettings = 0;
const int kIdLap = 19;
const int kIdRecord = 20;
const int kIdEvent = 21;
+const int kIdCourse = 31;
+const int kIdCoursePoint = 32;
+
+// constants for local IDs (for writing)
+const int kWriteLocalIdFileId = 0;
+const int kWriteLocalIdCourse = 1;
+const int kWriteLocalIdLap = 2;
+const int kWriteLocalIdEvent = 3;
+const int kWriteLocalIdCoursePoint = 4;
+const int kWriteLocalIdRecord = 5;
// constants for message fields
// for all global IDs
const int kFieldTimestamp = 253;
+const int kFieldMessageIndex = 254;
+// for global ID: file id
+const int kFieldType = 0;
+const int kFieldManufacturer = 1;
+const int kFieldProduct = 2;
+const int kFieldTimeCreated = 4;
// for global ID: device settings
const int kFieldGlobalUtcOffset = 4;
// for global ID: lap
const int kFieldEndLatitude = 5;
const int kFieldEndLongitude = 6;
const int kFieldElapsedTime = 7;
+const int kFieldTotalTimerTime = 8;
const int kFieldTotalDistance = 9;
+const int kFieldAvgSpeed = 13;
+const int kFieldMaxSpeed = 14;
// for global ID: record
const int kFieldLatitude = 0;
const int kFieldLongitude = 1;
const int kEnumEventTimer = 0;
const int kFieldEventType = 1;
const int kEnumEventTypeStart = 0;
+const int kFieldEventGroup = 4;
+// for global ID: course
+const int kFieldSport = 4;
+const int kFieldName = 5;
+// for global ID: course point
+const int kFieldCPTimeStamp = 1;
+const int kFieldCPPositionLat = 2;
+const int kFieldCPPositionLong = 3;
+const int kFieldCPDistance = 4;
+const int kFieldCPName = 6;
+const int kFieldCPType = 5;
// For developer fields as a non conflicting id
const int kFieldInvalid = 255;
+// types for message definitions
+const int kTypeEnum = 0x00;
+const int kTypeUint8 = 0x02;
+const int kTypeString = 0x07;
+const int kTypeUint16 = 0x84;
+const int kTypeSint32 = 0x85;
+const int kTypeUint32 = 0x86;
+
+// misc. constants for message fields
+const int kFileCourse = 0x06;
+const int kEventTimer = 0x00;
+const int kEventTypeStart = 0x00;
+const int kEventTypeStopDisableAll = 0x09;
+const int kCoursePointTypeGeneric = 0x00;
+const int kCoursePointTypeLeft = 0x06;
+const int kCoursePointTypeRight = 0x07;
+
+const int kWriteHeaderLen = 12;
+const int kWriteHeaderCrcLen = 14;
+
+const double kSynthSpeed = 10.0 * 1000 / 3600; /* speed in m/s */
+
static char* opt_allpoints = nullptr;
static int lap_ct = 0;
static bool new_trkseg = false;
+static bool write_header_msgs = false;
+
static
arglist_t fit_args[] = {
ARG_TERMINATOR
};
+const std::vector<std::pair<QString, int> > kCoursePointTypeMapping = {
+ {"left", kCoursePointTypeLeft},
+ {"links", kCoursePointTypeLeft},
+ {"gauche", kCoursePointTypeLeft},
+ {"izquierda", kCoursePointTypeLeft},
+ {"sinistra", kCoursePointTypeLeft},
+
+ {"right", kCoursePointTypeRight},
+ {"rechts", kCoursePointTypeRight},
+ {"droit", kCoursePointTypeRight},
+ {"derecha", kCoursePointTypeRight},
+ {"destro", kCoursePointTypeRight},
+};
+
+
typedef struct {
int id;
int size;
fit_message_def message_def[16];
} fit_data;
+struct FitCourseRecordPoint {
+ FitCourseRecordPoint(const Waypoint &wpt, bool is_course_point, unsigned int course_point_type = kCoursePointTypeGeneric)
+ : lat(wpt.latitude),
+ lon(wpt.longitude),
+ altitude(wpt.altitude),
+ speed(WAYPT_HAS((&wpt), speed) ? wpt.speed : -1),
+ odometer_distance(wpt.odometer_distance),
+ creation_time(wpt.creation_time),
+ shortname(wpt.shortname),
+ is_course_point(is_course_point),
+ course_point_type(course_point_type) { }
+ double lat, lon, altitude;
+ double speed, odometer_distance;
+ gpsbabel::DateTime creation_time;
+ QString shortname;
+ bool is_course_point;
+ unsigned int course_point_type;
+};
+
+std::deque<FitCourseRecordPoint> course, waypoints;
+
+
static gbfile* fin;
+static gbfile* fout;
/*******************************************************************************
* %%% global callbacks called by gpsbabel main process %%% *
gbfclose(fin);
}
+static void
+fit_wr_init(const QString& fname)
+{
+ fout = gbfopen_le(fname, "w+b", MYNAME);
+}
+
+static void
+fit_wr_deinit()
+{
+ gbfclose(fout);
+}
+
/*******************************************************************************
* fit_parse_header- parse the global FIT header
if (alt != 0xffff) {
waypt->altitude = (alt / 5.0) - 500;
}
- waypt->SetCreationTime(QDateTime::fromTime_t(timestamp + 631065600));
+ waypt->SetCreationTime(QDateTime::fromTime_t(GPS_Math_Gtime_To_Utime(timestamp)));
if (speed != 0xffff) {
WAYPT_SET(waypt, speed, speed / 1000.0f);
}
}
}
+/*******************************************************************************
+* FIT writing
+*******************************************************************************/
+
+const static std::vector<fit_field_t> fit_msg_fields_file_id = {
+ // field id, size, type
+ { kFieldType, 0x01, kTypeEnum },
+ { kFieldManufacturer, 0x02, kTypeUint16 },
+ { kFieldProduct, 0x02, kTypeUint16 },
+ { kFieldTimeCreated, 0x04, kTypeUint32 },
+};
+const static std::vector<fit_field_t> fit_msg_fields_course = {
+ { kFieldName, 0x10, kTypeString },
+ { kFieldSport, 0x01, kTypeEnum },
+};
+const static std::vector<fit_field_t> fit_msg_fields_lap = {
+ { kFieldTimestamp, 0x04, kTypeUint32 },
+ { kFieldStartTime, 0x04, kTypeUint32 },
+ { kFieldStartLatitude, 0x04, kTypeSint32 },
+ { kFieldStartLongitude, 0x04, kTypeSint32 },
+ { kFieldEndLatitude, 0x04, kTypeSint32 },
+ { kFieldEndLongitude, 0x04, kTypeSint32 },
+ { kFieldElapsedTime, 0x04, kTypeUint32 },
+ { kFieldTotalTimerTime, 0x04, kTypeUint32 },
+ { kFieldTotalDistance, 0x04, kTypeUint32 },
+ { kFieldAvgSpeed, 0x02, kTypeUint16 },
+ { kFieldMaxSpeed, 0x02, kTypeUint16 },
+};
+const static std::vector<fit_field_t> fit_msg_fields_event = {
+ { kFieldTimestamp, 0x04, kTypeUint32 },
+ { kFieldEvent, 0x01, kTypeEnum },
+ { kFieldEventType, 0x01, kTypeEnum },
+ { kFieldEventGroup, 0x01, kTypeUint8 },
+};
+const static std::vector<fit_field_t> fit_msg_fields_course_point = {
+ { kFieldCPTimeStamp, 0x04, kTypeUint32 },
+ { kFieldCPPositionLat, 0x04, kTypeSint32 },
+ { kFieldCPPositionLong, 0x04, kTypeSint32 },
+ { kFieldCPDistance, 0x04, kTypeUint32 },
+ { kFieldCPName, 0x10, kTypeString },
+ { kFieldCPType, 0x01, kTypeEnum },
+};
+const static std::vector<fit_field_t> fit_msg_fields_record = {
+ { kFieldTimestamp, 0x04, kTypeUint32 },
+ { kFieldLatitude, 0x04, kTypeSint32 },
+ { kFieldLongitude, 0x04, kTypeSint32 },
+ { kFieldDistance, 0x04, kTypeUint32 },
+ { kFieldAltitude, 0x02, kTypeUint16 },
+ { kFieldSpeed, 0x02, kTypeUint16 },
+};
+
+
+static void
+fit_write_message_def(uint8_t local_id, uint16_t global_id, const std::vector<fit_field_t> &fields) {
+ gbfputc(0x40 | local_id, fout); // Local ID
+ gbfputc(0, fout); // Reserved
+ gbfputc(0, fout); // Little endian
+ gbfputuint16(global_id, fout); // Global ID
+ gbfputc(fields.size(), fout); // Number of fields
+ for (auto &&field : fields) {
+ gbfputc(field.id, fout); // Field definition number
+ gbfputc(field.size, fout); // Field size in bytes
+ gbfputc(field.type, fout); // Field type
+ }
+}
+
+
+static uint16_t
+fit_crc16(uint8_t data, uint16_t crc) {
+ static const uint16_t crc_table[] = {
+ 0x0000, 0xcc01, 0xd801, 0x1400, 0xf001, 0x3c00, 0x2800, 0xe401,
+ 0xa001, 0x6c00, 0x7800, 0xb401, 0x5000, 0x9c01, 0x8801, 0x4400
+ };
+
+ crc = (crc >> 4) ^ crc_table[crc & 0xf] ^ crc_table[data & 0xf];
+ crc = (crc >> 4) ^ crc_table[crc & 0xf] ^ crc_table[(data >> 4) & 0xf];
+ return crc;
+}
+
+
+static void
+fit_write_timestamp(const gpsbabel::DateTime &t) {
+ uint32_t t_fit;
+ if (t.isValid() && t.toTime_t() >= (unsigned int)GPS_Math_Gtime_To_Utime(0)) {
+ t_fit = GPS_Math_Utime_To_Gtime(t.toTime_t());
+ } else {
+ t_fit = 0xffffffff;
+ }
+ gbfputuint32(t_fit, fout);
+}
+
+
+static void
+fit_write_fixed_string(const QString &s, unsigned int len) {
+ QString trimmed(s);
+ QByteArray u8buf;
+
+ // Truncate if too long, making sure not to chop in the middle of a UTF-8
+ // character (i.e. we chop the unicode string and then check whether its
+ // UTF-8 representation fits)
+ while (true) {
+ u8buf = trimmed.toUtf8();
+ if (static_cast<unsigned int>(u8buf.size()) < len) {
+ break;
+ }
+ trimmed.chop(1);
+ }
+ // If the string was too short initially or we had to chop multibyte
+ // characters, the UTF-8 representation might be too short now, so pad
+ // it.
+ u8buf.append(len - u8buf.size(), '\0');
+ gbfwrite(u8buf.data(), len, 1, fout);
+}
+
+
+static void
+fit_write_position(double pos) {
+ if (pos >= -180 && pos < 180) {
+ gbfputint32(GPS_Math_Deg_To_Semi(pos), fout);
+ } else {
+ gbfputint32(0xffffffff, fout);
+ }
+}
+
+
+// Note: The data fields written using fit_write_msg_*() below need to match
+// the message field definitions in fit_msg_fields_* above!
+static void
+fit_write_msg_file_id(uint8_t type, uint16_t manufacturer, uint16_t product,
+ const gpsbabel::DateTime &time_created) {
+ gbfputc(kWriteLocalIdFileId, fout);
+ gbfputc(type, fout);
+ gbfputuint16(manufacturer, fout);
+ gbfputuint16(product, fout);
+ fit_write_timestamp(time_created);
+}
+
+static void
+fit_write_msg_course(const QString &name, uint8_t sport) {
+ gbfputc(kWriteLocalIdCourse, fout);
+ fit_write_fixed_string(name, 0x10);
+ gbfputc(sport, fout);
+}
+
+static void
+fit_write_msg_lap(const gpsbabel::DateTime ×tamp, const gpsbabel::DateTime &start_time,
+ double start_position_lat, double start_position_long,
+ double end_position_lat, double end_position_long,
+ uint32_t total_elapsed_time_s, double total_distance_m,
+ double avg_speed_ms, double max_speed_ms) {
+ gbfputc(kWriteLocalIdLap, fout);
+ fit_write_timestamp(timestamp);
+ fit_write_timestamp(start_time);
+ fit_write_position(start_position_lat);
+ fit_write_position(start_position_long);
+ fit_write_position(end_position_lat);
+ fit_write_position(end_position_long);
+ if (total_elapsed_time_s < 4294967) {
+ gbfputuint32(total_elapsed_time_s * 1000, fout);
+ gbfputuint32(total_elapsed_time_s * 1000, fout);
+ } else {
+ gbfputuint32(0xffffffff, fout);
+ gbfputuint32(0xffffffff, fout);
+ }
+ if (total_distance_m >= 0 && total_distance_m < 42949672.94) {
+ gbfputuint32(total_distance_m * 100, fout);
+ } else {
+ gbfputuint32(0xffffffff, fout);
+ }
+ if (avg_speed_ms >= 0 && avg_speed_ms < 65.534) {
+ gbfputuint16(avg_speed_ms * 1000, fout);
+ } else {
+ gbfputuint16(0xffff, fout);
+ }
+ if (max_speed_ms >= 0 && max_speed_ms < 65.534) {
+ gbfputuint16(max_speed_ms * 1000, fout);
+ } else {
+ gbfputuint16(0xffff, fout);
+ }
+}
+
+
+static void
+fit_write_msg_event(const gpsbabel::DateTime ×tamp,
+ uint8_t event, uint8_t event_type, uint8_t event_group) {
+ gbfputc(kWriteLocalIdEvent, fout);
+ fit_write_timestamp(timestamp);
+ gbfputc(event, fout);
+ gbfputc(event_type, fout);
+ gbfputc(event_group, fout);
+}
+
+
+static void
+fit_write_msg_course_point(const gpsbabel::DateTime ×tamp,
+ double position_lat, double position_long,
+ double distance_m, const QString &name,
+ uint8_t type) {
+ gbfputc(kWriteLocalIdCoursePoint, fout);
+ fit_write_timestamp(timestamp);
+ fit_write_position(position_lat);
+ fit_write_position(position_long);
+ if (distance_m >= 0 && distance_m < 42949672.94) {
+ gbfputuint32(distance_m * 100, fout);
+ } else {
+ gbfputuint32(0xffffffff, fout);
+ }
+ fit_write_fixed_string(name, 0x10);
+ gbfputc(type, fout);
+}
+
+
+static void
+fit_write_msg_record(const gpsbabel::DateTime ×tamp,
+ double position_lat, double position_long,
+ double distance_m, double altitude,
+ double speed_ms) {
+ gbfputc(kWriteLocalIdRecord, fout);
+ fit_write_timestamp(timestamp);
+ fit_write_position(position_lat);
+ fit_write_position(position_long);
+ if (distance_m >= 0 && distance_m < 42949672.94) {
+ gbfputuint32(distance_m * 100, fout);
+ } else {
+ gbfputuint32(0xffffffff, fout);
+ }
+ if (altitude != unknown_alt && altitude >= -500 && altitude < 12606.8) {
+ gbfputuint16((altitude + 500) * 5, fout);
+ } else {
+ gbfputuint16(0xffff, fout);
+ }
+ if (speed_ms >= 0 && speed_ms < 65.534) {
+ gbfputuint16(speed_ms * 1000, fout);
+ } else {
+ gbfputuint16(0xffff, fout);
+ }
+}
+
+
+static void
+fit_write_file_header(uint32_t file_size, uint16_t crc)
+{
+ gbfputc(kWriteHeaderCrcLen, fout); // Header+CRC length
+ gbfputc(0x10, fout); // Protocol version
+ gbfputuint16(0x811, fout); // Profile version
+ gbfputuint32(file_size, fout); // Length of data records (little endian)
+ gbfputs(".FIT", fout); // Signature
+ gbfputuint16(crc, fout); // CRC
+}
+
+
+static void
+fit_write_header_msgs(gpsbabel::DateTime ctime, QString name)
+{
+ fit_write_message_def(kWriteLocalIdFileId, kIdFileId, fit_msg_fields_file_id);
+ fit_write_message_def(kWriteLocalIdCourse, kIdCourse, fit_msg_fields_course);
+ fit_write_message_def(kWriteLocalIdLap, kIdLap, fit_msg_fields_lap);
+ fit_write_message_def(kWriteLocalIdEvent, kIdEvent, fit_msg_fields_event);
+ fit_write_message_def(kWriteLocalIdCoursePoint, kIdCoursePoint, fit_msg_fields_course_point);
+ fit_write_message_def(kWriteLocalIdRecord, kIdRecord, fit_msg_fields_record);
+
+ fit_write_msg_file_id(kFileCourse, 1, 0x3e9, ctime);
+ fit_write_msg_course(name, 0);
+}
+
+
+static void
+fit_write_file_finish()
+{
+ // Update data records size in file header
+ gbsize_t file_size = gbftell(fout);
+ if (file_size < kWriteHeaderCrcLen) {
+ fatal(MYNAME ": File %s truncated\n", fout->name);
+ }
+ gbfseek(fout, 0, SEEK_SET);
+ fit_write_file_header(file_size - kWriteHeaderCrcLen, 0);
+
+ // Update file header CRC
+ uint16_t crc = 0;
+ gbfseek(fout, 0, SEEK_SET);
+ for (unsigned int i = 0; i < kWriteHeaderLen; i++) {
+ int data = gbfgetc(fout);
+ if (data == EOF) {
+ fatal(MYNAME ": File %s truncated\n", fout->name);
+ }
+ crc = fit_crc16(data, crc);
+ }
+ gbfseek(fout, 0, SEEK_SET);
+ fit_write_file_header(file_size - kWriteHeaderCrcLen, crc);
+
+ // Write file CRC
+ gbfflush(fout);
+ crc = 0;
+ while (true) {
+ int data = gbfgetc(fout);
+ if (data == EOF) {
+ break;
+ }
+ crc = fit_crc16(data, crc);
+ }
+ gbfputuint16(crc, fout);
+}
+
+static void
+fit_collect_track_hdr(const route_head *rte)
+{
+ (void)rte;
+ course.clear();
+}
+
+static void
+fit_collect_trackpt(const Waypoint* waypointp)
+{
+ course.push_back(FitCourseRecordPoint(*waypointp, false));
+}
+
+static void
+fit_collect_track_tlr(const route_head *rte)
+{
+ // Prepare for writing a course corresponding to a track.
+ // For this, we need to check for/synthesize missing information
+ // and convert waypoints to coursepoints (i.e. insert them at the right
+ // place between course records).
+
+ // Recalculate odometer_distance for the whole track unless already
+ // (properly, i.e. monotonically increasing) set
+ double dist_sum = 0;
+ double prev_lat = 999, prev_lon = 999;
+ double max_speed = 0;
+ gpsbabel::DateTime prev_time;
+ for (auto &crpt: course) {
+ // Distance to prev. point
+ double dist;
+ if (crpt.odometer_distance && crpt.odometer_distance >= dist_sum) {
+ dist = crpt.odometer_distance - dist_sum;
+ dist_sum = crpt.odometer_distance;
+ } else {
+ if (prev_lat >= -90 && prev_lat <= 90 && prev_lon >= -180 && prev_lon <= 180) {
+ dist = gcgeodist(prev_lat, prev_lon, crpt.lat, crpt.lon);
+ } else {
+ dist = 0;
+ }
+ dist_sum += dist;
+ crpt.odometer_distance = dist_sum;
+ }
+ prev_lat = crpt.lat;
+ prev_lon = crpt.lon;
+
+ // Check/set timestamp/speed
+ if (!crpt.creation_time.isValid() ||
+ (prev_time.isValid() && prev_time >= crpt.creation_time)) {
+ if (crpt.speed < 1e-3) {
+ crpt.speed = kSynthSpeed;
+ }
+ crpt.creation_time = prev_time.addSecs(dist / crpt.speed);
+ } else if (crpt.speed < 1e-3) {
+ uint64_t duration = prev_time.secsTo(crpt.creation_time);
+ if (!duration) {
+ duration = 1;
+ }
+ crpt.speed = dist / duration;
+ }
+ prev_time = crpt.creation_time;
+
+ if (crpt.speed > max_speed) {
+ max_speed = crpt.speed;
+ }
+ }
+
+ // Insert course points at the right place between track points (with
+ // minimum distance to next track point)
+ while (!waypoints.empty()) {
+ auto &wpt = waypoints.front();
+ double best_distance = -1;
+ auto best_distance_it = course.begin();
+ double best_odometer_distance = 0;
+ for (auto cit = course.begin(); cit != course.end(); cit++) {
+ if (!cit->is_course_point) {
+ double distance = gcgeodist(cit->lat, cit->lon, wpt.lat, wpt.lon);;
+ if (best_distance < 0 || distance < best_distance) {
+ best_distance = distance;
+ best_distance_it = cit;
+ best_odometer_distance = cit->odometer_distance;
+ }
+ }
+ }
+ wpt.odometer_distance = best_odometer_distance;
+ course.insert(best_distance_it, wpt);
+ waypoints.pop_front();
+ }
+
+ // Use current time as creation time if we have nothing better
+ gpsbabel::DateTime track_date_time, track_end_date_time, creation_time;
+ double first_lat = 999, first_lon = 999, last_lat = 999, last_lon = 999;
+ if (!course.empty()) {
+ track_date_time = creation_time = course.front().creation_time;
+ track_end_date_time = course.back().creation_time;
+ first_lat = course.front().lat;
+ first_lon = course.front().lon;
+ last_lat = course.back().lat;
+ last_lon = course.back().lon;
+ } else {
+ creation_time = gpsbabel::DateTime::currentDateTimeUtc();
+ }
+ uint32_t total_time = track_date_time.secsTo(track_end_date_time);
+
+ // Write file-level header messages here because we need a name and date
+ // and take these from the first track
+ if (write_header_msgs) {
+ fit_write_header_msgs(creation_time, rte->rte_name);
+ write_header_msgs = false;
+ }
+
+ // Write track header messages (lap+start event)
+ double avg_speed = 0;
+ if (total_time) {
+ avg_speed = dist_sum / total_time;
+ }
+ fit_write_msg_lap(track_date_time, track_date_time,
+ first_lat, first_lon, last_lat, last_lon, total_time, dist_sum,
+ avg_speed, max_speed);
+ fit_write_msg_event(track_date_time, kEventTimer, kEventTypeStart, 0);
+
+ // Write track/course points for the whole track
+ for (auto &crpt: course) {
+ if (crpt.is_course_point) {
+ fit_write_msg_course_point(crpt.creation_time,
+ crpt.lat,
+ crpt.lon,
+ crpt.odometer_distance,
+ crpt.shortname,
+ crpt.course_point_type);
+ } else {
+ fit_write_msg_record(crpt.creation_time,
+ crpt.lat,
+ crpt.lon,
+ crpt.odometer_distance,
+ crpt.altitude,
+ crpt.speed);
+ }
+ }
+
+ fit_write_msg_event(track_end_date_time, kEventTimer, kEventTypeStopDisableAll, 0);
+}
+
+static void
+fit_collect_waypt(const Waypoint* waypointp)
+{
+ FitCourseRecordPoint crpt(*waypointp, true);
+
+ // Try to find a better course point type than "generic", based on the
+ // course point name
+ for (auto &cptm: kCoursePointTypeMapping) {
+ if (crpt.shortname.contains(cptm.first, Qt::CaseInsensitive)) {
+ crpt.course_point_type = cptm.second;
+ break;
+ }
+ }
+
+ waypoints.push_back(crpt);
+}
+
+
+
+/*******************************************************************************
+* fit_write- global entry point
+*******************************************************************************/
+static void
+fit_write()
+{
+ fit_write_file_header(0, 0);
+ write_header_msgs = true;
+ waypt_disp_all(fit_collect_waypt);
+ track_disp_all(fit_collect_track_hdr, fit_collect_track_tlr, fit_collect_trackpt);
+ fit_write_file_finish();
+}
+
/**************************************************************************/
// capabilities below means: we can only read and write waypoints
ff_vecs_t format_fit_vecs = {
ff_type_file,
{
- ff_cap_none /* waypoints */,
- ff_cap_read /* tracks */,
- ff_cap_none /* routes */
+ ff_cap_write /* waypoints */,
+ (ff_cap)(ff_cap_read | ff_cap_write) /* tracks */,
+ ff_cap_none /* routes */
},
fit_rd_init,
- nullptr,
+ fit_wr_init,
fit_rd_deinit,
- nullptr,
+ fit_wr_deinit,
fit_read,
- nullptr,
+ fit_write,
nullptr,
fit_args,
CET_CHARSET_ASCII, 0 /* ascii is the expected character set */
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<gpx xmlns="http://www.topografix.com/GPX/1/1" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" version="1.1" creator="Mavi 2.11">
+ <wpt lat="30.048200" lon="-91.594200">
+ <name>Left</name>
+ </wpt>
+ <wpt lat="30.052600" lon="-91.594800">
+ <name>Right</name>
+ </wpt>
+ <wpt lat="30.052217" lon="-91.595017">
+ <name>rechts</name>
+ </wpt>
+ <wpt lat="30.051883" lon="-91.594700">
+ <name>links</name>
+ </wpt>
+ <wpt lat="30.051050" lon="-91.594400">
+ <name>turn right</name>
+ </wpt>
+ <wpt lat="30.050567" lon="-91.594233">
+ <name>gauche</name>
+ </wpt>
+ <wpt lat="30.050183" lon="-91.594100">
+ <name>izquierda</name>
+ </wpt>
+ <wpt lat="30.049100" lon="-91.593717">
+ <name>sinistra</name>
+ </wpt>
+ <wpt lat="30.048450" lon="-91.594250">
+ <name>droit</name>
+ </wpt>
+ <wpt lat="30.048083" lon="-91.594750">
+ <name>derecha</name>
+ </wpt>
+ <wpt lat="30.047500" lon="-91.595450">
+ <name>destro</name>
+ </wpt>
+ <wpt lat="30.047450" lon="-91.595200">
+ <name>Test</name>
+ </wpt>
+ <trk>
+ <name>meridian</name>
+ <trkseg>
+ <trkpt lat="30.062183" lon="-91.610350">
+ <ele>1</ele>
+ <time>2002-05-25T17:06:21.250000</time>
+ </trkpt>
+ <trkpt lat="30.062783" lon="-91.610567">
+ <time>2002-05-25T17:09:55.190000</time>
+ </trkpt>
+ <trkpt lat="30.062700" lon="-91.608267">
+ <time>2002-05-25T17:12:00.200000</time>
+ </trkpt>
+ <trkpt lat="30.062333" lon="-91.607383">
+ <time>2002-05-25T17:12:48.750000</time>
+ </trkpt>
+ <trkpt lat="30.061533" lon="-91.605283">
+ <time>2002-05-25T17:14:41.200000</time>
+ </trkpt>
+ <trkpt lat="30.059783" lon="-91.599400">
+ <time>2002-05-25T17:17:16.200000</time>
+ </trkpt>
+ <trkpt lat="30.057800" lon="-91.596683">
+ <time>2002-05-25T17:17:46.200000</time>
+ </trkpt>
+ <trkpt lat="30.055383" lon="-91.594900">
+ <time>2002-05-25T17:18:20.810000</time>
+ </trkpt>
+ <trkpt lat="30.053883" lon="-91.592617">
+ <time>2002-05-25T17:19:01.200000</time>
+ </trkpt>
+ <trkpt lat="30.049733" lon="-91.589750">
+ <time>2002-05-25T17:20:46.250000</time>
+ </trkpt>
+ <trkpt lat="30.049017" lon="-91.589883">
+ <time>2002-05-25T17:21:10.250000</time>
+ </trkpt>
+ <trkpt lat="30.048800" lon="-91.592933">
+ <time>2002-05-25T17:21:51.370000</time>
+ </trkpt>
+ <trkpt lat="30.046233" lon="-91.596450">
+ <time>2002-05-25T17:22:35.200000</time>
+ </trkpt>
+ <trkpt lat="30.045517" lon="-91.598717">
+ <time>2002-05-25T17:23:08.560000</time>
+ </trkpt>
+ <trkpt lat="30.047300" lon="-91.600267">
+ <time>2002-05-25T18:04:23.930000</time>
+ </trkpt>
+ <trkpt lat="30.047000" lon="-91.599633">
+ <ele>2</ele>
+ <time>2002-05-25T18:06:04.920000</time>
+ </trkpt>
+ <trkpt lat="30.046433" lon="-91.599467">
+ <time>2002-05-25T18:07:06.920000</time>
+ </trkpt>
+ <trkpt lat="30.046200" lon="-91.598950">
+ <ele>1</ele>
+ <time>2002-05-25T18:08:18.920000</time>
+ </trkpt>
+ <trkpt lat="30.046367" lon="-91.597733">
+ <time>2002-05-25T18:10:20.920000</time>
+ </trkpt>
+ <trkpt lat="30.046350" lon="-91.597167">
+ <time>2002-05-25T18:11:09.930000</time>
+ </trkpt>
+ <trkpt lat="30.046783" lon="-91.596333">
+ <time>2002-05-25T18:12:18.920000</time>
+ </trkpt>
+ <trkpt lat="30.047450" lon="-91.595200">
+ <time>2002-05-25T18:14:22.930000</time>
+ </trkpt>
+ <trkpt lat="30.047800" lon="-91.594767">
+ <ele>2</ele>
+ <time>2002-05-25T18:15:04.930000</time>
+ </trkpt>
+ <trkpt lat="30.048250" lon="-91.594083">
+ <ele>1</ele>
+ <time>2002-05-25T18:16:14.930000</time>
+ </trkpt>
+ <trkpt lat="30.048683" lon="-91.593800">
+ <ele>1</ele>
+ <time>2002-05-25T18:17:01.930000</time>
+ </trkpt>
+ <trkpt lat="30.049350" lon="-91.593850">
+ <time>2002-05-25T18:18:07.940000</time>
+ </trkpt>
+ <trkpt lat="30.050317" lon="-91.593983">
+ <ele>2</ele>
+ <time>2002-05-25T18:19:51.940000</time>
+ </trkpt>
+ <trkpt lat="30.050783" lon="-91.594117">
+ <time>2002-05-25T18:20:39.940000</time>
+ </trkpt>
+ <trkpt lat="30.051233" lon="-91.594367">
+ <time>2002-05-25T18:21:24.930000</time>
+ </trkpt>
+ <trkpt lat="30.051800" lon="-91.594367">
+ <time>2002-05-25T18:22:17.940000</time>
+ </trkpt>
+ <trkpt lat="30.052217" lon="-91.594667">
+ <time>2002-05-25T18:23:18.930000</time>
+ </trkpt>
+ <trkpt lat="30.053017" lon="-91.594683">
+ <time>2002-05-25T18:24:37.940000</time>
+ </trkpt>
+ <trkpt lat="30.054867" lon="-91.595200">
+ <ele>6</ele>
+ <time>2002-05-25T18:28:13.950000</time>
+ </trkpt>
+ <trkpt lat="30.053733" lon="-91.594933">
+ <ele>2</ele>
+ <time>2002-05-25T18:31:36.940000</time>
+ </trkpt>
+ <trkpt lat="30.053183" lon="-91.594783">
+ <time>2002-05-25T18:32:56.950000</time>
+ </trkpt>
+ <trkpt lat="30.052633" lon="-91.594833">
+ <time>2002-05-25T18:34:02.950000</time>
+ </trkpt>
+ <trkpt lat="30.052450" lon="-91.595433">
+ <time>2002-05-25T18:36:03.950000</time>
+ </trkpt>
+ <trkpt lat="30.052483" lon="-91.595967">
+ <time>2002-05-25T18:36:48.960000</time>
+ </trkpt>
+ <trkpt lat="30.052650" lon="-91.596783">
+ <ele>1</ele>
+ <time>2002-05-25T18:37:52.960000</time>
+ </trkpt>
+ <trkpt lat="30.053133" lon="-91.597850">
+ <time>2002-05-25T18:39:18.950000</time>
+ </trkpt>
+ <trkpt lat="30.053617" lon="-91.597967">
+ <time>2002-05-25T18:40:15.960000</time>
+ </trkpt>
+ <trkpt lat="30.053967" lon="-91.597767">
+ <ele>6</ele>
+ <time>2002-05-25T18:41:25.960000</time>
+ </trkpt>
+ <trkpt lat="30.053617" lon="-91.598083">
+ <time>2002-05-25T18:42:37.960000</time>
+ </trkpt>
+ <trkpt lat="30.053200" lon="-91.597917">
+ <time>2002-05-25T18:44:01.960000</time>
+ </trkpt>
+ <trkpt lat="30.052817" lon="-91.597517">
+ <time>2002-05-25T18:45:53.960000</time>
+ </trkpt>
+ <trkpt lat="30.052567" lon="-91.596933">
+ <time>2002-05-25T18:46:54.960000</time>
+ </trkpt>
+ <trkpt lat="30.052333" lon="-91.596433">
+ <time>2002-05-25T18:47:42.970000</time>
+ </trkpt>
+ <trkpt lat="30.052250" lon="-91.595683">
+ <time>2002-05-25T18:48:41.960000</time>
+ </trkpt>
+ <trkpt lat="30.052217" lon="-91.595017">
+ <time>2002-05-25T18:49:52.970000</time>
+ </trkpt>
+ <trkpt lat="30.051883" lon="-91.594700">
+ <time>2002-05-25T18:50:49.970000</time>
+ </trkpt>
+ <trkpt lat="30.051050" lon="-91.594400">
+ <time>2002-05-25T18:52:14.970000</time>
+ </trkpt>
+ <trkpt lat="30.050567" lon="-91.594233">
+ <time>2002-05-25T18:52:56.980000</time>
+ </trkpt>
+ <trkpt lat="30.050183" lon="-91.594100"/>
+ <trkpt lat="30.049100" lon="-91.593717"/>
+ <trkpt lat="30.048450" lon="-91.594250"/>
+ <trkpt lat="30.048083" lon="-91.594750"/>
+ <trkpt lat="30.047500" lon="-91.595450">
+ <ele>7</ele>
+ </trkpt>
+ <trkpt lat="30.047067" lon="-91.596000"/>
+ <trkpt lat="30.046633" lon="-91.596600"/>
+ <trkpt lat="30.046400" lon="-91.597650"/>
+ <trkpt lat="30.046233" lon="-91.598467"/>
+ </trkseg>
+ <trkseg>
+ <trkpt lat="30.046317" lon="-91.598967"/>
+ <trkpt lat="30.046783" lon="-91.599283"/>
+ <trkpt lat="30.047133" lon="-91.599667"/>
+ </trkseg>
+ </trk>
+</gpx>